#include <linux/module.h>
#include <linux/init.h>

#include <linux/fs.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

#define BLK_DEV_NUM_SECTORS     4096
#define BLK_DEV_SECTOR_SIZE     512
#define BLK_DEV_DISK_SIZE       (BLK_DEV_SECTOR_SIZE*BLK_DEV_NUM_SECTORS)

#define KERNEL_SECTOR_SIZE      512

/* Віртуальний блочний пристрій */
static struct blk_dev
{
    char *data; /* Пам'ять пристрою */
    unsigned long size; /* Розмір пристрою в байтах */
    spinlock_t lock;
    struct gendisk *gendisk;
    struct request_queue *queue; /* Черга запитів вводу/виводу */
};

static int blk_dev_open(struct block_device *bd, fmode_t mode)
{
    struct blk_dev *d = bd->bd_disk->private_data;

    return 0;
}

static int blk_dev_release(struct inode *i, struct file *filp)
{
    return 0;
}

static int blk_dev_ioctl(struct block_device *bd, fmode_t mode, unsigned int cmd, unsigned long arg)
{
    struct blk_dev *dev = bd->bd_disk->private_data;
    struct hd_geometry geo;

    switch (cmd) {
    case HDIO_GETGEO:
        geo.cylinders = (dev->size & ~0x3f) >> 6;
        geo.heads = 4;
        geo.sectors = 16;
        geo.start = 4;

        if (copy_to_user((void __user *)arg, &geo, sizeof(struct hd_geometry)))
        {
            printk("Fail copy from kernel space to user space\n");
            return -EFAULT;
        }
        return 0;
    }

    return -ENOTTY;
}

static struct block_device_operations blk_dev_ops = {
    .owner = THIS_MODULE,
    .open = blk_dev_open,
    .release = blk_dev_release,
    .ioctl = blk_dev_ioctl,
};

/* Обробники запитів вводу/виводу */
static void blk_dev_transfer(struct blk_dev *d, char *data, sector_t sector, unsigned long num_sectors, int write)
{
    unsigned long offset = sector * KERNEL_SECTOR_SIZE;
    unsigned long size = num_sectors * KERNEL_SECTOR_SIZE;
    printk("blk_dev_transfer() was called\n");
    if ((offset + size) > d->size)
    {
        printk("Fail access to sectors, which are out of the device\n");
        return;
    }
    if (write)
        memcpy(d->data + offset, data, size);
    else
        memcpy(data, d->data + offset, size);
}

static void blk_dev_queue_handle(struct request_queue *queue)
{
    struct request *req = blk_fetch_request(queue);
    struct blockdev *dev = queue->queuedata;

    while (req != NULL) {
        if (req->cmd_type != REQ_TYPE_FS) {
            __blk_end_request_all(req, -EIO);
            continue;
        }

        blk_dev_transfer(dev, req->buffer, blk_rq_pos(req), blk_rq_cur_sectors(req), rq_data_dir(req));

        if (!__blk_end_request_cur(req, 0))
            req = blk_fetch_request(queue);
    }
}

static struct blk_dev *dev;
static int blk_dev_major;

static int blk_dev_setup(struct blk_dev *d)
{
    d->queue = blk_init_queue(blk_dev_queue_handle, &d->lock);
    if (!d->queue)
        return -ENOMEM;
    d->queue->queuedata = d;
    blk_queue_logical_block_size(d->queue, BLK_DEV_SECTOR_SIZE);

    // blk_queue_hardsect_size(d->queue, 512); /* FIXME: hardcoded value */

    d->gendisk = alloc_disk(1);
    if (!d->gendisk)
        goto free_queue;

    d->data = vmalloc(d->size);
    if (!d->data)
        goto free_gendisk;

    d->gendisk->private_data = d;
    d->gendisk->major = blk_dev_major;
    d->gendisk->first_minor = 0; /* FIXME: hardcoded value */
    d->gendisk->queue = d->queue;
    d->gendisk->fops = &blk_dev_ops;
    snprintf(d->gendisk->disk_name, 32, "block_device"); /* FIXME: hardcoded value */
    set_capacity(d->gendisk, BLK_DEV_NUM_SECTORS); /* Вказуємо об'єм диска */
    add_disk(d->gendisk);

    return 0;

free_gendisk:
    del_gendisk(d->gendisk);
free_queue:
    blk_cleanup_queue(d->queue);

    return -ENOMEM;
}

static int blk_dev_init(void)
{
    int retval;

    /* регістрація блочного драйвера */
    blk_dev_major = register_blkdev(0, "block_device");
    if (blk_dev_major <= 0)
    {
        printk("Fail registration block driver\n");
        return -EBUSY;
    }

    dev = kmalloc(sizeof(struct blk_dev), GFP_KERNEL);
    if (!dev)
    {
        printk("Fail allocate device\n");
        goto unregister_blkdev;
    }

    spin_lock_init(&dev->lock);
    dev->size = BLK_DEV_DISK_SIZE;
    retval = blk_dev_setup(dev);
    if (retval)
    {
        printk("Fail setup device\n");
        goto free_dev;
    }

    return 0;

free_dev:
    kfree(dev);
unregister_blkdev:
    unregister_blkdev(blk_dev_major, "block_device");
    return -ENOMEM;
}

static void blk_dev_exit(void)
{
    blk_cleanup_queue(dev->queue);
    vfree(dev->data);
    del_gendisk(dev->gendisk);
    put_disk(dev->gendisk);
    kfree(dev);
    unregister_blkdev(blk_dev_major, "block_device");
}

module_init(blk_dev_init);
module_exit(blk_dev_exit);

MODULE_LICENSE("GPL");
